ระบบควบคุมการเบิก-จ่ายน้ำมันเชื้อเพลิงยานพาหนะ ที่ผูก การอนุมัติ (approval) เข้ากับ หัวจ่ายจริง (dispenser head) ด้วยอุปกรณ์ IoT — เอกสารนี้อธิบายว่าระบบมีอะไร ใครใช้ส่วนไหน ข้อมูลอยู่ที่ใด และกฎอะไรที่ทุกหน้าจอต้องเคารพร่วมกัน
ผู้อ่าน: ลูกค้าที่อนุมัติ business flow · ทีมพัฒนาที่จะสร้างระบบจริง · ทีม QA ที่เขียนแผนทดสอบ · ทีมงานใหม่ที่ต้องเข้าใจภาพรวม อ่านคู่กับ Doc - Business Flow (ลำดับงานตั้งแต่ต้นจนจบ) และ Doc - Kiosk Gateway (state machine ของตู้หัวจ่าย)
หัวใจของระบบคือประโยคเดียว: การอนุมัติเป็นเพียงข้อความ จนกระทั่งถูกผูกกับฮาร์ดแวร์ คำขอที่อนุมัติแล้วยังจ่ายน้ำมันไม่ได้ ต้องมีการผูก (bind) คำขอนั้นเข้ากับหัวจ่ายจริงที่ตู้คีออสก์ก่อน ทุกหน้าจอต้องทำให้ความต่างนี้อ่านได้ทันที
ต้นแบบนี้ใช้ store ในเบราว์เซอร์ตัวเดียว (oiltrack-db.js + oiltrack-seed.json) เป็นแหล่งความจริงเดียว ทุกหน้าจอ subscribe การเปลี่ยนแปลงและ re-render เอง ไม่มีหน้าไหนถือข้อมูลตัวอย่างของตัวเอง เมื่อระบบจริงมาแทน จุดต่อคือ API layer ที่ให้ฟังก์ชันชุดเดียวกันนี้
| ผู้ใช้งาน | เข้าสู่ระบบด้วย | ทำอะไรได้ | ทำไม่ได้ |
|---|---|---|---|
| ผู้ดูแลระบบ administrator |
อีเมล + รหัสผ่าน | ข้อมูลหลักทั้งหมด (รถ · สถานี · ถัง · ตู้จ่าย · สถานที่หน้างาน) · ผู้ใช้งานทุกบทบาท · มอบหมายรถ↔คนขับ · สั่งเปิด/ปิดการจ่ายของหัวจ่าย | อนุมัติคำขอ · บันทึกสต็อก |
| พนักงานบัญชี accountant |
อีเมล + รหัสผ่าน | อนุมัติ/ไม่อนุมัติคำขอ · แก้ลิตรที่อนุมัติ · ต่อเวลา · ปลดหัวจ่าย · ยกเลิกคำขอ · มอบหมายคำขอจัดส่ง · บันทึกสต็อกและปรับปรุงยอด · ตรวจสอบธงระยะทาง · รายงาน | แก้ข้อมูลหลัก · เพิ่ม/ลบผู้ใช้งาน |
| คนขับรถ driver |
เบอร์โทร + OTP | ยื่นคำขอสำหรับรถที่ตนรับผิดชอบ · ยกเลิกคำขอของตน · สแกน QR ที่หัวจ่ายและยืนยันการผูก · ดูประวัติของตน | เห็นความจุถังเป็นตัวเลข · เห็นธงระยะทางหรือผลตรวจสอบ · เห็นคำขอของคนอื่น · อนุมัติคำขอของตน |
| ตู้คีออสก์ gateway (อุปกรณ์) |
ผูกกับหัวจ่ายด้วย gatewayId | แสดง QR/รหัส · จอง (reserve) หัวจ่ายเมื่อสแกน · เดินมาตรวัดและปิดงานเมื่อครบเพดานหรือหยุดจ่ายครบเวลา | รับ input จากคนที่หน้าตู้ (อ่านอย่างเดียว) · ยกเลิกคำขอ |
ทั้งระบบมี 16 ตาราง แบ่งเป็นสามชั้น: ข้อมูลหลัก (คนกรอก) · รายการเดินเรื่อง (เกิดจากงานประจำวัน) · สต็อกที่ระบบสร้างเอง (ไม่มีหน้าจอไหนเขียนตรง)
| ตาราง | คีย์ | คืออะไร | ใครเขียน |
|---|---|---|---|
| ข้อมูลหลัก · master data | |||
| administrators | ผู้ดูแลระบบ | ผู้ดูแลระบบ | |
| accountants | พนักงานบัญชี | ผู้ดูแลระบบ | |
| drivers | phoneNo | คนขับ + สถานะลงทะเบียน/OTP | ผู้ดูแลระบบ · OTP โดยระบบ |
| cars | licenseNo | รถ · ชนิดเชื้อเพลิง · ความจุ · คนขับที่รับผิดชอบ · tankId เมื่อติดถังเคลื่อนที่ | ผู้ดูแลระบบ |
| stations · worksites | id · code | สถานีบริการ และสถานที่หน้างาน (พร้อมพิกัดอ้างอิงสำหรับธงระยะทาง) | ผู้ดูแลระบบ |
| tanks | id | ถัง ประจำสถานี (site) หรือ ติดรถ (mobile) + ความจุ | ผู้ดูแลระบบ |
| dispensers | id · code | หัวจ่าย + หัวจ่ายย่อย (nozzles) · gatewayId · dispenseEnabled | ผู้ดูแลระบบ |
| รายการเดินเรื่อง · transactional | |||
| requests | REQ-nnnn | คำขอเบิกน้ำมัน — สถานะ · ลิตรที่ขอ/อนุมัติ/จ่ายจริง · หัวจ่ายที่ผูก · กำหนดเวลา · geoPoints · หมายเหตุภายใน · ต้นทุน | คนขับ · บัญชี · คีออสก์ |
| dispatchRequests | MD-nnnn | คำขอจัดส่ง — รวมคำขอหลายใบเข้ารถน้ำมันเคลื่อนที่หนึ่งคัน + คำขอเติมถัง (top-up) | บัญชี |
| stockRecords | SR-nnnn | รายการบันทึกสต็อก — incoming / add / deduct ชนิดรายการเป็นตัวกำหนดทิศทาง | บัญชี · ระบบ (การจ่าย) |
| devices · sweepLog · session · config | — | สิทธิ์บนเครื่องคนขับ · บันทึกการกวาดล่าสุด 50 รายการ · เซสชันที่เข้าสู่ระบบ · ค่าคอนฟิกและตัวนับเลขที่ | ระบบ |
| สต็อกที่ระบบสร้างเอง · engine-owned (อ่านเท่านั้น) | |||
| stockLots | LOT-nnnn | คิว FIFO หนึ่งล็อตต่อหนึ่งรายการฝั่งเพิ่ม — ที่เดียวที่เก็บต้นทุนต่อลิตร | stock engine |
| stockUsages | SU-nnnn | หนึ่งแถวต่อการตัดสต็อกหนึ่งครั้ง พร้อมการปันส่วนรายล็อต ต้นทุนรวม และบริบทของคำขอ | stock engine |
| liveStock | tankId | ยอดคงเหลือปัจจุบันที่คำนวณไว้แล้ว — หน้าจออ่านค่าเดียวจบ ไม่ต้องรวมประวัติ | stock engine |
คำขอมี 10 สถานะ แบ่งเป็นสามกลุ่ม: มีชีวิต (live — ยังถือทรัพยากรอยู่) · ผูกฮาร์ดแวร์ (bound — ถือหัวจ่ายด้วย) · ปิดเรื่องแล้ว (terminal) หน้าจอต้องอ่านจาก DB.statusMeta() เท่านั้น ห้ามเขียนรายการสถานะเองในหน้าจอ
รถ — คำขอที่มีชีวิตหนึ่งใบล็อกรถคันนั้นสำหรับทุกคน ตั้งแต่รออนุมัติเป็นต้นไป รถที่ถูกล็อกยังแสดงในรายการของคนขับแต่เป็นสีเทาพร้อมเหตุผล ไม่ซ่อน · คนขับ — หนึ่งคำขอที่มีชีวิตต่อคน · หัวจ่าย — หนึ่งการผูกต่อหัว การผูกซ้ำถูกปฏิเสธด้วยเหตุผล dispenser-in-use การปล่อยทุกกรณีไปทางเดียวคือ releaseRequest() ซึ่งล้างการผูก กำหนดเวลา และช่องคำขอจัดส่งในการเขียนครั้งเดียว ยกเลิกแล้วคนขับยื่นใหม่ได้ทันที
น้ำมันที่รับเข้าแต่ละครั้งเปิดเป็น ล็อต ที่มีต้นทุนต่อลิตรของตัวเอง การจ่ายออกตัดจากล็อตเก่าที่สุดก่อน และการจ่ายครั้งเดียวอาจข้ามหลายล็อต — ต้นทุนของคำขอนั้นจึงเป็นค่าเฉลี่ยถ่วงน้ำหนักตามที่ตัดจริง
สามกรณีที่ต้องเข้าใจ: สต็อกไม่พอ — ยังจ่ายได้ ส่วนที่เกินคิดต้นทุนด้วยราคาล่าสุดที่รู้ และแถวนั้นถูกตั้งธง ต้นทุนไม่ครบ ให้บัญชีตามแก้ด้วยการรับเข้าย้อนหลัง · โอนเข้ารถน้ำมันเคลื่อนที่ — ต้นทุนติดไปกับน้ำมัน ล็อตบนรถจึงไม่ใช่ ฿0 · การจ่ายตามคำขอ — เป็นชนิดรายการที่ระบบเขียนเองเท่านั้น ไม่มีในตัวเลือกของฟอร์ม และไม่ปนอยู่ในหน้ารายการบันทึกสต็อก
| ค่าคอนฟิก | ค่า | นับจากอะไร | หมดเวลาแล้วเกิดอะไร |
|---|---|---|---|
| reviewExpiryMinutes | 240 | คนขับยื่นคำขอ | หมดอายุ · ปล่อยรถและคนขับ |
| approvalExpiryMinutes | 120 | อนุมัติ (และเริ่มใหม่เมื่อส่งงานจัดส่ง) | หมดอายุ · ปล่อยรถและคนขับ |
| totpWindowSeconds | 30 | นาฬิกาของหัวจ่าย | รหัส 6 หลักเปลี่ยน ไม่มีช่วงผ่อนผัน |
| bindConfirmMinutes | 2 | สแกนสำเร็จ (จองหัวจ่าย) | กลับเป็น อนุมัติแล้ว · ปลดหัวจ่าย |
| fuelStartMinutes | 5 | ยืนยันการผูกในแอป | ยกเลิก คำขอ · ปลดหัวจ่าย |
| fuelIdleTimeoutMinutes | 1 | หยุดจ่ายกลางคัน | ปิดงานด้วยลิตรที่จ่ายจริง → เสร็จสิ้น |
| geoFlagRadiusMeters | 500 | พิกัดอ้างอิงของแต่ละเหตุการณ์ | ตั้งธงให้บัญชีตรวจ — ไม่บล็อกคนขับ |
การบังคับใช้ทำสองทางโดยตั้งใจ: ตู้คีออสก์บังคับสด เพราะเป็นเจ้าของหน้าจอ และ DB.sync() เป็นตัวสำรองสำหรับตู้ที่ออฟไลน์ รีบูต หรือไม่ได้เปิดอยู่ ทั้งสองทางเรียกฟังก์ชันเดียวกันจึงได้ผลเหมือนกัน — และเรียกซ้ำได้ไม่เปลี่ยนผล (idempotent) นอกจากกวาดหมดอายุ sync() ยังปิดคำขอจัดส่งที่คำขอในนั้นจบหรือถูกปล่อยหมดแล้ว และเขียนเครดิตน้ำมันเข้าถังเคลื่อนที่เมื่อคำขอเติมถังเสร็จ
| ไทย | English / code | ความหมาย |
|---|---|---|
| คำขอเบิกน้ำมัน | request · REQ-nnnn | หน่วยงานหลักของระบบ หนึ่งใบ = การเติมหนึ่งครั้ง |
| ผูกหัวจ่าย | bind | จับคู่คำขอที่อนุมัติแล้วกับหัวจ่ายจริง สองขั้น: สแกน → ยืนยันในแอป |
| ปลดหัวจ่าย | unbind | บัญชีคืนหัวจ่ายให้ว่าง คำขอกลับไปรอผูกใหม่ |
| หัวจ่าย / หัวจ่ายย่อย | dispenser · nozzle | ตู้จ่ายหนึ่งตู้มีหัวจ่ายย่อย 1–4 หัว แต่ละหัวผูกกับถังและชนิดน้ำมันหนึ่งอย่าง |
| รถน้ำมันเคลื่อนที่ | mobile fuel truck | รถที่ติดถังและหัวจ่าย ออกไปเติมให้เครื่องจักรที่หน้างาน |
| คำขอจัดส่ง | dispatch · MD-nnnn | งานหนึ่งรอบของรถน้ำมันเคลื่อนที่ รวมคำขอหลายใบ + คำขอเติมถังของตัวรถ |
| เติมถังเคลื่อนที่ | top-up | คำขอที่ระบบสร้างให้คนขับรถน้ำมัน เพื่อเติมถังบนรถก่อนออกงาน (0 ลิตร = ไม่ต้องเติม) |
| ล็อตสต็อก | lot · LOT-nnnn | น้ำมันหนึ่งชุดที่รับเข้าพร้อมต้นทุนต่อลิตรของมัน |
| รายการใช้งานสต็อก | usage · SU-nnnn | การตัดสต็อกหนึ่งครั้ง พร้อมรายละเอียดว่าตัดจากล็อตไหนเท่าไร |
| ธงระยะทาง | distance flag | พิกัดที่ห่างจากจุดอ้างอิงเกินรัศมี — เป็นคำขอให้ตรวจสอบ ไม่ใช่การบล็อก |
| การกวาด | sweep · DB.sync() | รอบตรวจที่ปิดคำขอหมดอายุ ปิดงานจัดส่งที่ว่างเปล่า และเขียนเครดิตที่ค้าง |
| รหัส 6 หลักที่หัวจ่าย | TOTP | รหัสที่หมุนทุก 30 วินาที ใช้แทนการสแกน QR เมื่อกล้องใช้ไม่ได้ |